Which NBA teams are best at developing their players?
Sam Benning
JOUR479X Final Presentation
This presentation takes a bit of a turn on my first project, where I assessed which statistical measures are useful for predicting an NBA career. I continued to work with time-series-based data, looking at player development. Overall, I wanted to analyze the system of drafting and improving players throughout the NBA, analyzing by different teams, positions, and players.
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.5
## ✔ forcats 1.0.0 ✔ stringr 1.5.1
## ✔ ggplot2 3.5.1 ✔ tibble 3.2.1
## ✔ lubridate 1.9.3 ✔ tidyr 1.3.1
## ✔ purrr 1.0.2
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(dplyr)
library(ggplot2)
library(ggrepel)
library(gt)
library(plotly)
##
## Attaching package: 'plotly'
##
## The following object is masked from 'package:ggplot2':
##
## last_plot
##
## The following object is masked from 'package:stats':
##
## filter
##
## The following object is masked from 'package:graphics':
##
## layout
library(cowplot)
##
## Attaching package: 'cowplot'
##
## The following object is masked from 'package:gt':
##
## as_gtable
##
## The following object is masked from 'package:lubridate':
##
## stamp
library(gt)
I scraped data from basketball reference, three different data sets from years 2005-2025 - ‘basic’ stats, advanced stats, and each team’s NBA draft history. They are all combined into one file that includes a separate observation for a player from every season they played, with each year as an entry. Their draft data (team, rd, pick, traded), is all included as well.
all_data <- read.csv("https://raw.githubusercontent.com/sbenning7/jour479x_fall_2024/refs/heads/main/presentations/presentation2/nba_0525.csv")
head(all_data)
## X Player Age MP Rk Team Pos G GS PER TS. X3PAr FTr ORB. DRB.
## 1 1 A.J. Hammons 24 163 416 DAL C 22 0 8.4 0.472 0.238 0.476 5.4 21.0
## 2 2 A.J. Price 23 865 277 IND PG 56 2 14.0 0.530 0.492 0.212 1.5 9.7
## 3 3 A.J. Price 24 795 290 IND PG 50 0 10.7 0.454 0.466 0.253 2.3 7.8
## 4 4 A.J. Price 25 568 313 IND PG 44 1 11.5 0.454 0.506 0.201 2.6 9.4
## 5 5 A.J. Price 26 1278 220 WAS PG 57 22 12.4 0.501 0.484 0.150 1.7 8.2
## 6 6 A.J. Price 27 99 435 MIN SG 28 0 9.7 0.469 0.478 0.043 1.1 10.2
## TRB. AST. STL. BLK. TOV. USG. OWS DWS WS WS.48 OBPM DBPM BPM VORP Awards
## 1 12.8 3.8 0.3 7.2 16.4 17.6 -0.2 0.2 0.0 -0.001 -6.6 0.0 -6.6 -0.2
## 2 5.6 20.5 2.0 0.2 13.2 22.7 0.4 0.8 1.2 0.065 -0.1 -0.4 -0.5 0.3
## 3 5.0 22.7 1.8 0.1 13.0 22.7 -0.4 0.7 0.3 0.020 -1.9 -1.1 -2.9 -0.2
## 4 6.0 23.9 1.9 0.3 14.5 17.7 0.2 0.5 0.7 0.063 -0.6 -0.5 -1.0 0.1
## 5 4.9 26.4 1.3 0.2 12.7 18.0 1.0 1.2 2.2 0.084 -0.2 -0.4 -0.6 0.5
## 6 5.5 21.5 0.5 0.0 13.0 23.2 -0.1 0.0 0.0 -0.012 -2.4 -2.4 -4.8 -0.1
## Year FG FGA FG. X3P X3PA X3P. X2P X2PA X2P. eFG. FT FTA FT. ORB DRB
## 1 2017 17 42 0.405 5 10 0.500 12 32 0.375 0.464 9 20 0.450 8 28
## 2 2010 145 354 0.410 60 174 0.345 85 180 0.472 0.494 60 75 0.800 12 76
## 3 2011 114 320 0.356 41 149 0.275 73 171 0.427 0.420 54 81 0.667 16 56
## 4 2012 59 174 0.339 26 88 0.295 33 86 0.384 0.414 28 35 0.800 13 48
## 5 2013 161 413 0.390 70 200 0.350 91 213 0.427 0.475 49 62 0.790 20 94
## 6 2014 19 46 0.413 6 22 0.273 13 24 0.542 0.478 0 2 0.000 1 9
## TRB AST STL BLK TOV PF PTS Trp.Dbl Draft_Yr Rd Pick College Team_Drafted
## 1 36 4 1 13 10 21 48 0 2016 2 46 Purdue DAL
## 2 88 106 35 3 59 53 410 0 2009 2 52 UConn IND
## 3 72 111 29 1 53 61 323 0 2009 2 52 UConn IND
## 4 61 86 20 2 32 30 172 0 2009 2 52 UConn IND
## 5 114 205 33 3 64 73 441 0 2009 2 52 UConn IND
## 6 10 13 1 0 7 5 44 0 2009 2 52 UConn IND
## Traded Team_1 Team_2 First_Team years_pro years_with_team
## 1 0 <NA> <NA> DAL 0 1
## 2 0 <NA> <NA> IND 0 4
## 3 0 <NA> <NA> IND 1 4
## 4 0 <NA> <NA> IND 2 4
## 5 0 <NA> <NA> IND 3 4
## 6 0 <NA> <NA> IND 4 4
#Compute Top Players with Draft Year
top_players <- all_data |>
group_by(First_Team, Player) |>
filter(years_pro < 3, G > 20, MP > 200) |>
summarize(
per = mean(PER, na.rm = TRUE),
draft_year = unique(Draft_Yr),
pick = unique(Pick),
.groups = "drop"
) |>
arrange(First_Team, desc(per)) |>
group_by(First_Team) |>
mutate(rank = row_number()) |>
filter(rank <= 3) |> # Top 3 players per team
summarize(
top_players = paste0(Player, " (", round(per, 1), ", ", draft_year, " ", "Pick ", pick, ")",
collapse = "; "
),
.groups = "drop"
)
#Calculate Team PER
teamdrafting <- all_data |>
group_by(First_Team, Player) |>
filter(years_pro < 3, G > 20, MP > 200) |>
summarize(
per = mean(PER, na.rm = TRUE),
.groups = "drop"
) |>
group_by(First_Team) |>
summarize(
teamper = mean(per),
.groups = "drop"
) |>
arrange(desc(teamper)) |>
left_join(top_players, by = "First_Team")
#Format Styled Table
styled_table <- teamdrafting |>
select(Team = First_Team, `Team PER` = teamper, `Top Players` = top_players) |>
gt() |>
tab_header(
title = "Good Drafting = Nothing?",
subtitle = "The NBA best and worst teams over the past 20 years boast strong drafting abilities."
) |>
cols_align(align = "center", columns = everything()) |>
fmt_number(
columns = `Team PER`,
decimals = 2
) |>
cols_label(
Team = "Team",
`Team PER` = "Avg Player PER (First 3yrs of Career)",
`Top Players` = "Top 3 Players (PER, Draft Info)"
) |>
tab_style(
style = list(cell_fill(color = "#f5f5f5")),
locations = cells_body(columns = `Top Players`)
) |>
tab_source_note(
source_note = md("**By:** Sam Benning | **Source:** [Basketball Reference](https://www.basketball-reference.com/)")
) |>
tab_options(
table.border.top.color = "gray",
table.border.bottom.color = "gray",
heading.align = "center",
column_labels.font.weight = "bold"
)
# Display the styled table
styled_table
| Good Drafting = Nothing? | ||
| The NBA best and worst teams over the past 20 years boast strong drafting abilities. | ||
| Team | Avg Player PER (First 3yrs of Career) | Top 3 Players (PER, Draft Info) |
|---|---|---|
| NOP | 14.14 | Anthony Davis (26.3, 2012 Pick 1); Zion Williamson (25.6, 2019 Pick 1); Chris Paul (24.1, 2005 Pick 4) |
| HOU | 14.11 | Clint Capela (19.9, 2014 Pick 25); Montrezl Harrell (19.5, 2015 Pick 32); Carl Landry (19, 2007 Pick 31) |
| PHI | 13.87 | Joel Embiid (24.1, 2014 Pick 3); Paul Reed (20.3, 2020 Pick 58); Ben Simmons (20, 2016 Pick 1) |
| DAL | 13.72 | Luka Dončić (24.2, 2018 Pick 3); Nick Fazekas (19.7, 2007 Pick 34); Isaiah Roby (15.1, 2019 Pick 45) |
| GSW | 13.33 | Trayce Jackson-Davis (21.6, 2023 Pick 57); Stephen Curry (19, 2009 Pick 7); Brandan Wright (18, 2007 Pick 8) |
| DEN | 13.33 | Nikola Jokić (23.9, 2014 Pick 41); Kenneth Faried (20.1, 2011 Pick 22); Ty Lawson (17.9, 2009 Pick 18) |
| ATL | 13.21 | Trae Young (21.3, 2018 Pick 5); John Collins (21.2, 2017 Pick 19); Onyeka Okongwu (18.7, 2020 Pick 6) |
| NYK | 13.14 | Mitchell Robinson (21.1, 2018 Pick 36); Willy Hernangómez (20.5, 2015 Pick 35); Kristaps Porziņģis (18.5, 2015 Pick 4) |
| SAC | 13.08 | Neemias Queta (22.9, 2021 Pick 39); Tyrese Haliburton (19.1, 2020 Pick 12); DeMarcus Cousins (18.8, 2010 Pick 5) |
| TOR | 13.03 | Scottie Barnes (17.1, 2021 Pick 4); Ed Davis (16.6, 2010 Pick 13); Delon Wright (16.4, 2015 Pick 20) |
| OKC | 12.83 | Kevin Durant (20.9, 2007 Pick 2); Chet Holmgren (20.4, 2022 Pick 2); Russell Westbrook (18.9, 2008 Pick 4) |
| MIN | 12.71 | Karl-Anthony Towns (24.4, 2015 Pick 1); Kevin Love (21.1, 2008 Pick 5); Gorgui Dieng (16.9, 2013 Pick 21) |
| BRK | 12.67 | Day'Ron Sharpe (19.5, 2021 Pick 29); Brook Lopez (19.1, 2008 Pick 10); Jarrett Allen (18.9, 2017 Pick 22) |
| SAS | 12.48 | Victor Wembanyama (23.1, 2023 Pick 1); DeJuan Blair (17.5, 2009 Pick 37); Kawhi Leonard (17.5, 2011 Pick 15) |
| CHI | 12.44 | Daniel Gafford (20.9, 2019 Pick 38); Derrick Rose (19.4, 2008 Pick 1); Joakim Noah (16.6, 2007 Pick 9) |
| DET | 12.42 | Andre Drummond (21.9, 2012 Pick 9); Greg Monroe (19.8, 2010 Pick 7); Luka Garza (19.3, 2021 Pick 52) |
| UTA | 12.40 | Tony Bradley (21.7, 2017 Pick 28); Walker Kessler (19.8, 2022 Pick 22); Jeremy Evans (19.4, 2010 Pick 55) |
| CLE | 12.35 | Kyrie Irving (21, 2011 Pick 1); Evan Mobley (18, 2021 Pick 3); Danny Green (15.5, 2009 Pick 46) |
| IND | 12.31 | Isaiah Jackson (19.8, 2021 Pick 22); Myles Turner (16.8, 2015 Pick 11); Roy Hibbert (16.1, 2008 Pick 17) |
| CHA | 12.26 | Mark Williams (19.8, 2022 Pick 15); LaMelo Ball (18.4, 2020 Pick 3); Sean May (17.2, 2005 Pick 13) |
| MIL | 12.13 | John Henson (18, 2012 Pick 14); Jon Leuer (16.5, 2011 Pick 40); Brandon Jennings (16.2, 2009 Pick 10) |
| LAL | 12.12 | Thomas Bryant (21, 2017 Pick 42); Marc Gasol (18, 2007 Pick 48); Ivica Zubac (17.8, 2016 Pick 32) |
| ORL | 12.07 | Paolo Banchero (16.1, 2022 Pick 1); Franz Wagner (16.1, 2021 Pick 8); Kyle O'Quinn (15.7, 2012 Pick 49) |
| MEM | 12.05 | Brandon Clarke (20.6, 2019 Pick 21); Ja Morant (19.5, 2019 Pick 2); Ivan Rabb (16.4, 2017 Pick 35) |
| POR | 11.92 | Greg Oden (20.6, 2007 Pick 1); Brandon Roy (20.5, 2006 Pick 6); Damian Lillard (18.6, 2012 Pick 6) |
| BOS | 11.89 | Robert Williams (22.6, 2018 Pick 27); Ante Žižić (20.2, 2016 Pick 23); Leon Powe (17.6, 2006 Pick 49) |
| PHO | 11.87 | Deandre Ayton (20.3, 2018 Pick 1); Jalen Smith (17.6, 2020 Pick 10); T.J. Warren (15.1, 2014 Pick 14) |
| MIA | 11.79 | Bam Adebayo (18, 2017 Pick 14); Michael Beasley (16.3, 2008 Pick 2); Precious Achiuwa (14, 2020 Pick 20) |
| WAS | 11.69 | John Wall (18.1, 2010 Pick 1); JaVale McGee (17.1, 2008 Pick 18); Trevor Booker (14.9, 2010 Pick 23) |
| LAC | 11.33 | Blake Griffin (22.6, 2009 Pick 1); Shai Gilgeous-Alexander (17.6, 2018 Pick 11); Eric Gordon (15.8, 2008 Pick 7) |
| By: Sam Benning | Source: Basketball Reference | ||
This chart gives us a good general idea of who is drafting solid guys - taking just players early career PER in my opinion was the best way to show this. I wouldn’t have predicted New Orleans at the top, but it makes sense given that they have drafted three true superstar players in recent history, which is very rare across the league. Right behind them is Houston, who doesn’t boast any superstars at the top of their list, which shows some consistency.
If we want to take a peek into long term development, we can look at a team’s ability to develop players over time. After doing some thinking, I figured that you could gain a lot of information based upon someone’s rookie PER compared to their peak PER instead of their full career PER (because some players PER tanks at the end of their career).
I took the three current championship-level teams, and three mediocre teams (these teams also happened to be some of the best and worst in terms of PER growth). I will look at where they drafted players, and then how strong their PER difference is. This shows us who is drafting “diamonds in the rough” – who might have the ability to find players with potential, and then develop them. What’s the secret to success, and failure in terms of player personnel?
#selecting the teams and getting the data
#chose players who weren't traded on draft night
teams_pd <- all_data |> filter(First_Team %in% c("GSW", "DEN", "WAS", "BOS", "BRK", "DET")) %>% filter(Traded == 0) |> filter(G > 10) |> filter(years_with_team >1) |> group_by(Player, First_Team)|>
summarize(
pick = mean(Pick),
games = sum(G),
min = sum(MP),
draftyr = mean(Draft_Yr), #make this an average so that every year of a player's career isn't shown
lastyr = max(Year), #shows how long a player's career lasted when next to draft year
rookie_PER = first(PER),
peak_PER = max(PER, na.rm = TRUE),
PER_growth = peak_PER - rookie_PER,
)
## `summarise()` has grouped output by 'Player'. You can override using the
## `.groups` argument.
# Define team colors for formatting
team_colors <- c(
"GSW" = "yellow",
"DEN" = "blue3",
"BOS" = "green4",
"WAS" = "#E03A3E",
"BRK" = "black",
"DET" = "#C8102E"
)
# Reorder the Team_Drafted variable for visual
teams_pd <- teams_pd %>%
mutate(
First_Team = factor(First_Team, levels = c("GSW", "DEN", "BOS", "WAS", "BRK", "DET"))
)
# Create the plot
ggplot() +
geom_point(
data = teams_pd,
aes(x = draftyr, y = pick, size = PER_growth, color = First_Team),
alpha = 0.7
) +
facet_wrap(~First_Team) +
geom_text_repel(data=teams_pd,
aes(x=draftyr, y=pick, label=Player), size = 1.25) +
scale_y_reverse() +
scale_color_manual(values = team_colors) + # Use the custom color palette
labs(
title = "There's More Than One Way to do it",
subtitle = "Both good and bad teams can develop players well - what matters is everything else.",
x = "Draft Year",
y = "Draft Pick",
size = "PER Growth",
color = "Team"
) +
theme_minimal() +
theme(
strip.text = element_text(face = "bold", size = 12), # Bold facet titles
plot.title = element_text(size = 16, face = "bold"), # Bold title
legend.position = "right"
)
The story that I see is that the Pistons and Wizards are actually decent at player development - their chart doesn’t look much different from GSW/BOS in fact. I think it reflects poor roster management as a reason that they can’t succeed. Off the paper, it is clear that these teams just don’t do well externally.
The conclusion that I come to is that in order to build a top-level team, you need both good drafting and development internally, and roster additions in free agency/trades. I mean, that seems pretty logical to me.
This makes the Warriors franchise truly stand out to me though - they didn’t dip much into other players when they had their dynasty (aside from Durant).
We will furhter explore if each team has a particular specialty when it comes to development, looking specifically at each position. This helps show us where a team’s developmental success is actually coming from. Again, PER growth is our stat here.
# Analyze Player Development by Position
development_by_position <- all_data |>
group_by(Team, Player, Pos) |>
arrange(Player, Year) |> # Ensure data is ordered by year
mutate(
years_with_team = cumsum(Team == lag(Team, default = first(Team))) # Increment tenure
) |>
filter(years_with_team > 1, G > 20) |> # Only include players with more than 1 year with a team
summarize(
first_per = PER[Year == min(Year)], # PER in the first year with the team
peak_per = max(PER, na.rm = TRUE), # Peak PER with the team
improvement = peak_per - first_per, # Improvement
years_with_team = max(years_with_team),
.groups = "drop")
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
#Table to display best developers by position
team_pos_dev <- development_by_position |>
group_by(Team, Pos) |>
summarize(
avg_starting_per = round(mean(first_per, na.rm = TRUE), 2),
avg_peak_per = round(mean(peak_per, na.rm = TRUE), 2),
avg_improvement = round(mean(improvement, na.rm = TRUE), 2),
player_count = n(),
.groups = "drop"
) |>
arrange(desc(avg_improvement)) |> slice_head(n=10)
# Create the GT table
team_pos_dev |>
gt() |>
cols_label(
avg_starting_per = "Avg. PER entering team",
avg_peak_per = "Avg. Peak PER while with team (after development)",
avg_improvement = "Avg. Improvement",
player_count = "# of Players"
) |>
tab_header(
title = "GSW Stars with Curry's Development",
subtitle = "These 10 teams have excelled at helping players at certain positions improve their Player Efficiency Rating (PER) since 2005."
) |>
tab_style(
style = cell_text(color = "black", weight = "bold", align = "left"),
locations = cells_title("title")
) |>
tab_style(
style = cell_text(color = "black", align = "left"),
locations = cells_title("subtitle")
) |>
tab_style(
style = cell_text(weight = "bold"), # Bold the Avg. Improvement column
locations = cells_body(columns = "avg_improvement")
) |>
tab_options(
table.font.size = px(12), # Adjust font size for readability
heading.align = "left", # Align header text to the left
table.border.top.color = "black", # Add a border at the top of the table
table.border.bottom.color = "black" # Add a border at the bottom of the table
) |>
tab_source_note(
source_note = md("**By:** Sam Benning | **Source:** [Basketball Reference](https://www.basketball-reference.com/)")
)
| GSW Stars with Curry's Development | |||||
| These 10 teams have excelled at helping players at certain positions improve their Player Efficiency Rating (PER) since 2005. | |||||
| Team | Pos | Avg. PER entering team | Avg. Peak PER while with team (after development) | Avg. Improvement | # of Players |
|---|---|---|---|---|---|
| GSW | PG | 15.35 | 21.40 | 6.05 | 2 |
| TOR | SG | 10.35 | 13.65 | 3.30 | 4 |
| ATL | C | 16.05 | 18.50 | 2.45 | 6 |
| MIA | PG | 11.68 | 14.07 | 2.40 | 4 |
| POR | SG | 12.20 | 14.50 | 2.30 | 10 |
| CHI | PF | 15.43 | 17.67 | 2.24 | 7 |
| SAS | SF | 12.20 | 14.44 | 2.24 | 7 |
| SAS | PG | 13.80 | 15.91 | 2.11 | 7 |
| IND | C | 16.23 | 18.31 | 2.09 | 8 |
| GSW | SG | 11.00 | 13.06 | 2.06 | 8 |
| By: Sam Benning | Source: Basketball Reference | |||||
I find it valuable to see the full picture of how each player has stacked up for their respective teams. A chart that features individual development will dive deeper into why a team stands where they are growth-wise.
# Summarize total improvement for sorting
team_totals <- development_by_position %>%
group_by(Team) %>%
summarize(total_improvement = sum(improvement))
# Reorder teams by total improvement
development_by_position <- development_by_position %>% filter(Team != "2TM", Team != "3TM") |>
mutate(Team = factor(Team, levels = team_totals$Team[order(team_totals$total_improvement)]))
# Create the plot
totalimprovement <- ggplot(development_by_position, aes(
x = Team,
y = improvement,
fill = Pos,
text = paste0(Player, ", Improved PER by ", improvement, " (", years_with_team, " yrs)")
)) +
geom_bar(stat = "identity", color = "gray90", size = 0.2, position = "stack") + # Add border for separation
coord_flip() +
labs(
title = "Player Improvement by Position (Using PER Growth as a Measure)",
subtitle = "",
x = "Team",
y = "PER Growth While With Team"
) +
theme_minimal() +
theme(
legend.position = "right",
axis.text.y = element_text(size = 10), # Adjust text size for readability
panel.grid.major.y = element_line(color = "gray80") # Optional grid for better alignment
) +
guides(fill = guide_legend(title = "Position"))
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
### MAKING MY OWN FACET WRAP - R was being annoying
pg_dev <- development_by_position |> filter(Pos == "PG")
sg_dev <- development_by_position |> filter(Pos == "SG")
sf_dev <- development_by_position |> filter(Pos == "SF")
pf_dev <- development_by_position |> filter(Pos == "PF")
c_dev <- development_by_position |> filter(Pos == "C")
# Helper function to create a plot for a specific position
create_position_plot <- function(data, position) {
ggplot(data, aes(
x = reorder(Team, improvement, FUN = sum), # Reorder teams by improvement
y = improvement,
fill = Team,
text = paste0(Player, ", Improved PER by ", improvement, " (", years_with_team, " yrs)")
)) +
geom_bar(stat = "identity", color = "gray90", size = 0.25, position = "stack") +
coord_flip() +
labs(
title = paste(position),
x = "Team",
y = "PER Growth While With Team (Individual players included)"
) +
theme_minimal() +
theme(
legend.position = "none", # Hide legend for individual plots
axis.text.y = element_text(size = 10), # Adjust for better readability
panel.grid.major.y = element_line(color = "gray80")
)
}
# Create individual plots for each position
pg_plot <- create_position_plot(pg_dev, "Point Guard")
sg_plot <- create_position_plot(sg_dev, "Shooting Guard")
sf_plot <- create_position_plot(sf_dev, "Small Forward")
pf_plot <- create_position_plot(pf_dev, "Power Forward")
c_plot <- create_position_plot(c_dev, "Center")
#Plotting full graph, and each individual positional graph together
ggplotly(totalimprovement, tooltip = "text")
ggplotly(pg_plot, tooltip = "text")
ggplotly(sg_plot, tooltip = "text")
ggplotly(sf_plot, tooltip = "text")
ggplotly(pf_plot, tooltip = "text")
ggplotly(c_plot, tooltip = "text")
There could be players who happen to just develop on their own - we want to give players credit for their own hard work, too. I next will look into players who have been on two or more teams in their career and will visualize who has been the best at improving themselves regardless of the team that they are on.
#First, mutate to count the number of different teams a player has been with
all_data <- all_data %>%
group_by(Player) %>%
mutate(
num_teams = n_distinct(Team, na.rm = TRUE) # Count unique teams for each player
) %>%
ungroup() |>
filter(!Team %in% c("2TM", "3TM", "4TM", "5TM")) |> #remove the variables that show both teams combined stats
filter(!Year %in% 2025) #helps filter out players who might be doing well this year (small sample size)
#Filter for players who have played with \>1 team and record their stats as we have been doing
moved_players <- all_data |> filter(num_teams >=2) %>% filter(G > 20)|> group_by(Player)|>
summarize(
games = sum(G),
min = sum(MP),
seasons = max(Year) - mean(Draft_Yr),
teams = mean(num_teams),
rookie_PER = first(PER),
peak_PER = max(PER, na.rm = TRUE),
PER_growth = peak_PER - rookie_PER,
) |>
arrange(desc(PER_growth))
Here are 8 of the top 10 players who developed very well on their own over the past 20 years regardless of team:
# Filter data for a specific player
player_data <- all_data %>%
filter(Player == "Domantas Sabonis" | Player == "James Harden" | Player == "Shai Gilgeous-Alexander" | Player == "Russell Westbrook" | Player == "Jimmy Butler" | Player == "Lou Williams" | Player == "Terry Rozier" | Player == "Kevin Durant") %>%
mutate(yrspro = Year - Draft_Yr) |>
group_by(Year) |>
arrange(yrspro)
# Create the line chart
ggplot(player_data, aes(x = yrspro, y = PER, color = Team)) +
geom_line(size = 1) + # Line for career trajectory
geom_point(size = 3) + # Points for individual seasons
labs(
title = "OKC is the Place to be",
subtitle = "4 of the 8 current NBA players with the best career PER growth have played for OKC.",
x = "Years Pro",
y = "PER"
) +
facet_wrap(~Player) +
geom_text_repel(data=player_data, aes(x=yrspro, y=PER, label=Team), size = 2) +
theme_minimal() + # Minimal theme for clean visualization
theme(
plot.title = element_text(hjust = 0.5, face = "bold", size = 16),
axis.title = element_text(size = 14),
axis.text = element_text(size = 12)
)
I was looking for who had the best career trajectory and really stuck out the grind regardless of where they were. I was still left with my main takeaway coming from a team!
The OKC Thunder have clearly grown many stars through their organization. Obviously we are aware of the players who have come up through their organizations, but it is cool to visually see how players grow year-over-year as they progress with OKC. If we go back to the player development measures from before, they are in the top 10 of teams when it comes to those measures, too. The team knows what they are doing.
Another thing to credit these players who grew - (aside from the Thunder), a lot of growth occurred during years where a team wasn’t contending. Rozier on Boston’s crappy teams, then Charlotte…Lou Will on the Sixers…Butler with the Bulls (they got good as he got good)… and Sabonis with the Pacers. It seems like the ideal environment for the average player to make a name for themself is on a middling team. It makes me look forward to watching future drafts and trying to identify potential future stars.
If I were to keep going in this project, I would want to look at teams front offices as a whole and measure how good they are, using things like drafting, trading, contract handling, winning, etc. The research will continue as far as I can take it.